// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using Microsoft.MixedReality.WebRTC.Interop;

namespace Microsoft.MixedReality.WebRTC
{
    /// <summary>
    /// Type of media track or media transceiver.
    /// </summary>
    /// <remarks>
    /// This is the projection of <c>mrsMediaKind</c> from the interop API.
    /// </remarks>
    public enum MediaKind : uint
    {
        /// <summary>
        /// Audio data.
        /// </summary>
        Audio = 0,

        /// <summary>
        /// Video data.
        /// </summary>
        Video = 1
    }

    /// <summary>
    /// Delegate for the <see cref="Transceiver.Associated"/> event.
    /// </summary>
    /// <param name="transceiver">The transceiver becoming associated.</param>
    public delegate void TransceiverAssociatedDelegate(Transceiver transceiver);

    /// <summary>
    /// Delegate for the <see cref="Transceiver.DirectionChanged"/> event.
    /// </summary>
    /// <param name="transceiver">
    /// The transceiver whose <see cref="Transceiver.NegotiatedDirection"/> property changed.
    /// </param>
    public delegate void TransceiverDirectionChangedDelegate(Transceiver transceiver);

    /// <summary>
    /// Transceiver of a peer connection.
    ///
    /// A transceiver is a media "pipe" connecting the local and remote peers, and used to transmit media
    /// data (audio or video) between the peers. The transceiver has a media flow direction indicating whether
    /// it is sending and/or receiving any media, or is inactive. When sending some media, the transceiver's
    /// local track is used as the source of that media. Conversely, when receiving some media, that media is
    /// delivered to the remote media track of the transceiver. As a convenience, the local track can be null
    /// if the local peer does not have anything to send. In that case some empty media is automatically sent
    /// instead (black frames for video, silence for audio) at very reduced rate. To completely stop sending,
    /// the media direction must be changed instead.
    ///
    /// Transceivers are owned by the peer connection which creates them, and cannot be destroyed nor removed
    /// from the peer connection. They become invalid when the peer connection is closed, and should not be
    /// used after that.
    /// </summary>
    /// <remarks>
    /// This object corresponds roughly to the same-named notion in the WebRTC 1.0 standard when using the
    /// Unified Plan SDP semantic.
    ///
    /// For Plan B semantic, where RTP transceivers are not available, this wrapper tries to emulate the
    /// transceiver concept of the Unified Plan semantic, and is therefore providing an abstraction over the
    /// WebRTC concept of transceivers.
    /// </remarks>
    /// <seealso cref="PeerConnection.AddTransceiver(MediaKind, TransceiverInitSettings)"/>
    /// <seealso cref="PeerConnection.Close"/>
    public class Transceiver
    {
        /// <summary>
        /// Direction of the media flowing inside the transceiver.
        /// </summary>
        public enum Direction : int
        {
            /// <summary>
            /// Transceiver is both sending to and receiving from the remote peer connection.
            /// </summary>
            SendReceive = 0,

            /// <summary>
            /// Transceiver is sending to the remote peer, but is not receiving any media from the remote peer.
            /// </summary>
            SendOnly = 1,

            /// <summary>
            /// Transceiver is receiving from the remote peer, but is not sending any media to the remote peer.
            /// </summary>
            ReceiveOnly = 2,

            /// <summary>
            /// Transceiver is inactive, neither sending nor receiving any media data.
            /// </summary>
            Inactive = 3,
        }

        /// <summary>
        /// A name for the transceiver, used for logging and debugging only.
        /// This can be set on construction if the transceiver is created by the local peer using
        /// <see cref="PeerConnection.AddTransceiver(MediaKind, TransceiverInitSettings)"/>, or will
        /// be generated by the implementation otherwise.
        /// There is no guarantee of unicity; this name is only informational.
        /// </summary>
        public string Name { get; } = string.Empty;

        /// <summary>
        /// Type of media carried by the transceiver, and by extension type of media of its tracks.
        /// </summary>
        public MediaKind MediaKind { get; }

        /// <summary>
        /// Peer connection this transceiver is part of.
        /// </summary>
        /// <seealso cref="WebRTC.PeerConnection"/>
        public PeerConnection PeerConnection { get; } = null;

        /// <summary>
        /// Index of the media line in the SDP protocol for this transceiver. If the transceiver is not
        /// yet associated with a media line, this index has a negative value (invalid). Transceivers are
        /// associated when an offer, local or remote, is applied to the local peer connection. Consequently,
        /// transceivers created as a result of applying a remote offer are created in an associated state,
        /// with a media line index already valid, while transceivers created locally by the peer connection
        /// have an invalid index until the next offer.
        /// </summary>
        /// <remarks>
        /// For Plan B semantic (<see cref="SdpSemantic.PlanB"/>), the media line index is not present
        /// in the SDP protocol. Instead it is simulated by the implementation, which attempts to emulate
        /// the behavior of the Unified Plan semantic over an actual Plan B protocol.
        /// </remarks>
        /// <seealso cref="Associated"/>
        public int MlineIndex { get; internal set; } = -1;

        /// <summary>
        /// Event raised when the transceiver is associated with a media line, which therefore makes
        /// the <see cref="MlineIndex"/> property take a valid positive value.
        /// </summary>
        /// <remarks>
        /// The event is not raised if the transceiver is created in an associated state, that is if
        /// the transceiver is already associated when <see cref="PeerConnection.TransceiverAdded"/>
        /// is raised to signal it was added. This happens when the transceiver is created as part of
        /// applying a remote offer. In short, this event is raised only for transceivers created locally
        /// with <see cref="PeerConnection.AddTransceiver(MediaKind, TransceiverInitSettings)"/>.
        /// </remarks>
        /// <seealso cref="MlineIndex"/>
        public event TransceiverAssociatedDelegate Associated;

        /// <summary>
        /// Transceiver direction desired by the user.
        ///
        /// Once changed by the user, this value is the next direction that will be negotiated when
        /// calling <see cref="PeerConnection.CreateOffer"/> or <see cref="PeerConnection.CreateAnswer"/>.
        ///
        /// After the negotiation is completed, this is generally equal to <see cref="NegotiatedDirection"/>,
        /// unless the offer was partially rejected, for example if the local peer offered to send and receive
        /// some media but the remote peer only accepted to receive.
        ///
        /// Changing the value of this property triggers a <see cref="PeerConnection.RenegotiationNeeded"/> event.
        /// </summary>
        /// <seealso cref="NegotiatedDirection"/>
        public Direction DesiredDirection
        {
            get { return _desiredDirection; }
            set
            {
                if (value == _desiredDirection)
                {
                    return;
                }
                var res = TransceiverInterop.Transceiver_SetDirection(_nativeHandle, value);
                Utils.ThrowOnErrorCode(res);
                _desiredDirection = value;
            }
        }

        /// <summary>
        /// Last negotiated transceiver direction. This is constant when changing <see cref="DesiredDirection"/>,
        /// and is only udpated after an SDP session negotiation. This might be different from the desired
        /// direction if for example the local peer asked to receive but the remote peer refused. This is the
        /// actual direction the media is effectively transported in at any point in time.
        /// </summary>
        /// <seealso cref="DesiredDirection"/>
        public Direction? NegotiatedDirection { get; protected set; } = null;

        /// <summary>
        /// Event raised when the <see cref="NegotiatedDirection"/> changed, which occurs after applying
        /// a local or remote description. This is a convenience event raised only when the direction effectively
        /// changed, to avoid having to parse all transceivers for change after each description was applied.
        /// </summary>
        public event TransceiverDirectionChangedDelegate DirectionChanged;

        /// <summary>
        /// List of stream IDs associated with the transceiver.
        /// </summary>
        public string[] StreamIDs { get; }

        /// <summary>
        /// Local track attached to the transceiver, which is used to send data to the remote peer
        /// if <see cref="NegotiatedDirection"/> includes sending.
        /// This cannot be assigned directly; instead use <see cref="LocalAudioTrack"/> or
        /// <see cref="LocalVideoTrack"/> depending on the media kind of the transceiver.
        /// </summary>
        /// <seealso cref="LocalAudioTrack"/>
        /// <seealso cref="LocalVideoTrack"/>
        public LocalMediaTrack LocalTrack => _localTrack;

        /// <summary>
        /// Local audio track attached to the transceiver, if <see cref="MediaKind"/> is
        /// <see cref="MediaKind.Audio"/>.
        ///
        /// The property has two uses:
        /// - as a convenience getter to retrieve <see cref="LocalTrack"/> already cast to a
        ///   <see cref="WebRTC.LocalAudioTrack"/> type, or <c>null</c> if the transceiver
        ///   media kind is <see cref="MediaKind.Video"/>.
        /// - to attach a new local audio track if the transceiver is an audio transceiver;
        ///   otherwise this throws a <see cref="System.ArgumentException"/>.
        /// </summary>
        public LocalAudioTrack LocalAudioTrack
        {
            get { return (_localTrack as LocalAudioTrack); }
            set
            {
                if (MediaKind == MediaKind.Audio)
                {
                    SetLocalTrackImpl(value);
                }
                else
                {
                    throw new ArgumentException("Cannot assign local audio track as local track of video transceiver");
                }
            }
        }

        /// <summary>
        /// Local video track attached to the transceiver, if <see cref="MediaKind"/> is
        /// <see cref="MediaKind.Video"/>.
        ///
        /// The property has two uses:
        /// - as a convenience getter to retrieve <see cref="LocalTrack"/> already cast to a
        ///   <see cref="WebRTC.LocalVideoTrack"/> type, or <c>null</c> if the transceiver
        ///   media kind is <see cref="MediaKind.Audio"/>.
        /// - to attach a new local video track if the transceiver is a video transceiver;
        ///   otherwise this throws a <see cref="System.ArgumentException"/>.
        /// </summary>
        public LocalVideoTrack LocalVideoTrack
        {
            get { return (_localTrack as LocalVideoTrack); }
            set
            {
                if (MediaKind == MediaKind.Video)
                {
                    SetLocalTrackImpl(value);
                }
                else
                {
                    throw new ArgumentException("Cannot assign local video track as local track of audio transceiver");
                }
            }
        }

        /// <summary>
        /// Remote track attached to the transceiver, which is used to receive data from the
        /// remote peer if <see cref="NegotiatedDirection"/> includes receiving.
        /// This cannot be assigned. This is updated automatically when the remote track is
        /// created or destroyed as part of a renegotiation.
        /// </summary>
        /// <seealso cref="RemoteAudioTrack"/>
        /// <seealso cref="RemoteVideoTrack"/>
        public MediaTrack RemoteTrack => _remoteTrack;

        /// <summary>
        /// Remote audio track attached to the transceiver, if <see cref="MediaKind"/> is <see cref="MediaKind.Audio"/>.
        /// This is equivalent to <see cref="RemoteTrack"/> for audio transceivers, and <c>null</c> otherwise.
        /// </summary>
        public RemoteAudioTrack RemoteAudioTrack
        {
            get { return (_remoteTrack as RemoteAudioTrack); }
            internal set { _remoteTrack = value; }
        }

        /// <summary>
        /// Remote video track attached to the transceiver, if <see cref="MediaKind"/> is <see cref="MediaKind.Video"/>.
        /// This is equivalent to <see cref="RemoteTrack"/> for video transceivers, and <c>null</c> otherwise.
        /// </summary>
        public RemoteVideoTrack RemoteVideoTrack
        {
            get { return (_remoteTrack as RemoteVideoTrack); }
            internal set { _remoteTrack = value; }
        }

        /// <summary>
        /// Backing field for <see cref="DesiredDirection"/>.
        /// </summary>
        /// <seealso cref="DesiredDirection"/>
        protected Direction _desiredDirection;

        /// <summary>
        /// Handle to the native transceiver object, valid until <see cref="CleanUpAfterNativeDestroyed"/>
        /// is called by the native implementation when the transceiver is destroyed as part
        /// of the peer connection closing.
        /// </summary>
        /// <remarks>
        /// In native land this is a <code>mrsTransceiverHandle</code>.
        /// </remarks>
        internal TransceiverInterop.TransceiverHandle _nativeHandle = null;

        /// <summary>
        /// Reference to the struct keeping the callback delegates alive while registered with
        /// the native implementation.
        /// This should be released with <see cref="Utils.ReleaseWrapperRef(IntPtr)"/>.
        /// </summary>
        /// <seealso cref="TransceiverInterop.RegisterCallbacks(Transceiver, out IntPtr)"/>
        private IntPtr _argsRef = IntPtr.Zero;

        private LocalMediaTrack _localTrack = null;
        private MediaTrack _remoteTrack = null;

        /// <summary>
        /// Create a new transceiver associated with a given peer connection.
        /// </summary>
        /// <param name="handle">Handle to the native transceiver object.</param>
        /// <param name="mediaKind">The media kind of the transceiver and its tracks.</param>
        /// <param name="peerConnection">The peer connection owning this transceiver.</param>
        /// <param name="mlineIndex">The transceiver media line index in SDP.</param>
        /// <param name="name">The transceiver name.</param>
        /// <param name="streamIDs">Collection of stream IDs the transceiver is associated with, as set by the peer which created it.</param>
        /// <param name="initialDesiredDirection">Initial value to initialize <see cref="DesiredDirection"/> with.</param>
        internal Transceiver(TransceiverInterop.TransceiverHandle handle, MediaKind mediaKind, PeerConnection peerConnection, int mlineIndex,
            string name, string[] streamIDs, Direction initialDesiredDirection)
        {
            Debug.Assert(!handle.IsClosed);
            _nativeHandle = handle;
            MediaKind = mediaKind;
            PeerConnection = peerConnection;
            MlineIndex = mlineIndex;
            Name = name;
            StreamIDs = streamIDs;
            _desiredDirection = initialDesiredDirection;
            TransceiverInterop.RegisterCallbacks(this, out _argsRef);
        }

        /// <summary>
        /// Change the local audio track sending data to the remote peer.
        ///
        /// This detaches the previous local audio track if any, and attaches the new one instead.
        /// Note that the transceiver will only send some audio data to the remote peer if its
        /// negotiated direction includes sending some data and it has an attached local track to
        /// produce this data.
        ///
        /// This change is transparent to the session, and does not trigger any renegotiation.
        /// </summary>
        /// <param name="track">The new local audio track attached to the transceiver, and used to
        /// produce audio data to send to the remote peer if the transceiver is sending.
        /// Passing <c>null</c> is allowed, and will detach the current track if any.</param>
        private void SetLocalTrackImpl(LocalMediaTrack track)
        {
            if (track == _localTrack)
            {
                return;
            }

            var audioTrack = (track as LocalAudioTrack);
            var videoTrack = (track as LocalVideoTrack);
            if ((audioTrack != null) && (MediaKind != MediaKind.Audio))
            {
                throw new ArgumentException("Cannot set local audio track as local track of video transceiver");
            }
            if ((videoTrack != null) && (MediaKind != MediaKind.Video))
            {
                throw new ArgumentException("Cannot set local video track as local track of audio transceiver");
            }

            if (track != null)
            {
                if ((track.PeerConnection != null) && (track.PeerConnection != PeerConnection))
                {
                    throw new InvalidOperationException($"Cannot set track {track} of peer connection {track.PeerConnection} on transceiver {this} of different peer connection {PeerConnection}.");
                }
                uint res = Utils.MRS_E_UNKNOWN;
                if (audioTrack != null)
                {
                    res = TransceiverInterop.Transceiver_SetLocalAudioTrack(_nativeHandle, audioTrack._nativeHandle);
                }
                else if (videoTrack != null)
                {
                    res = TransceiverInterop.Transceiver_SetLocalVideoTrack(_nativeHandle, videoTrack._nativeHandle);
                }
                Utils.ThrowOnErrorCode(res);
            }
            else
            {
                // Note: Cannot pass null for SafeHandle parameter value (ArgumentNullException)
                uint res = Utils.MRS_E_UNKNOWN;
                if (MediaKind == MediaKind.Audio)
                {
                    res = TransceiverInterop.Transceiver_SetLocalAudioTrack(_nativeHandle, new LocalAudioTrackHandle());
                }
                else if (MediaKind == MediaKind.Video)
                {
                    res = TransceiverInterop.Transceiver_SetLocalVideoTrack(_nativeHandle, new LocalVideoTrackHandle());
                }
                Utils.ThrowOnErrorCode(res);
            }

            // Remove old track
            if (_localTrack != null)
            {
                Debug.Assert(_localTrack.Transceiver == this);
                Debug.Assert(_localTrack.PeerConnection == PeerConnection);
                _localTrack.Transceiver = null;
                _localTrack.PeerConnection = null;
                _localTrack = null;
            }

            // Add new track
            if (track != null)
            {
                Debug.Assert(track.Transceiver == null);
                Debug.Assert(track.PeerConnection == null);
                _localTrack = track;
                _localTrack.Transceiver = this;
                _localTrack.PeerConnection = PeerConnection;
            }
        }

        /// <summary>
        /// Callback invoked after the native transceiver has been destroyed, for clean-up.
        /// This is called by the peer connection when it closes, just before the C# transceiver
        /// object instance is destroyed.
        ///
        /// This replaces an hypothetical TransceiverRemoved callback, which doesn't exist to
        /// prevent confusion and underline the fact transceiver cannot be removed after being
        /// added to a peer connection, until that peer connection is closed and destroys them.
        /// </summary>
        internal void CleanUpAfterNativeDestroyed()
        {
            // The native peer connection was destroyed, therefore all its transceivers and remote
            // tracks too, since it was owning them. However local tracks are owned by the user, so
            // are possibly still alive.
            Debug.Assert(!_nativeHandle.IsClosed);
            Debug.Assert(_remoteTrack == null);
            if (_localTrack != null)
            {
                Debug.Assert(_localTrack.Transceiver == this);
                _localTrack.Transceiver = null;
                _localTrack = null;
            }
            _nativeHandle.Dispose();
            // No need (and can't) unregister callbacks, the native transceiver is already destroyed
            Utils.ReleaseWrapperRef(_argsRef);
            _argsRef = IntPtr.Zero;
        }

        /// <summary>
        /// Callback on internal implementation notifying that the transceiver was associated with
        /// a media line.
        /// </summary>
        /// <param name="mlineIndex">The index of the new media line the transceiver is associated with.</param>
        internal void OnAssociated(int mlineIndex)
        {
            Debug.Assert(MlineIndex < 0); // no recycling; otherwise this could be already valid
            Debug.Assert(mlineIndex >= 0);
            MlineIndex = mlineIndex;
            Associated?.Invoke(this);
        }

        /// <summary>
        /// Callback on internal implementation state changed to synchronize the cached state of this wrapper.
        /// </summary>
        /// <param name="negotiatedDirection">Current negotiated direction of the transceiver</param>
        /// <param name="desiredDirection">Current desired direction of the transceiver</param>
        internal void OnStateUpdated(Direction? negotiatedDirection, Direction desiredDirection)
        {
            _desiredDirection = desiredDirection;

            if (negotiatedDirection == NegotiatedDirection)
            {
                return;
            }

            bool hadSendBefore = HasSend(NegotiatedDirection);
            bool hasSendNow = HasSend(negotiatedDirection);
            bool hadRecvBefore = HasRecv(NegotiatedDirection);
            bool hasRecvNow = HasRecv(negotiatedDirection);

            NegotiatedDirection = negotiatedDirection;

            if (hadSendBefore != hasSendNow)
            {
                _localTrack?.OnMute(!hasSendNow);
            }
            if (hadRecvBefore != hasRecvNow)
            {
                _remoteTrack?.OnMute(!hasRecvNow);
            }

            DirectionChanged?.Invoke(this);
        }

        /// <summary>
        /// Check whether the given direction includes sending.
        /// </summary>
        /// <param name="dir">The direction to check.</param>
        /// <returns><c>true</c> if direction is <see cref="Direction.SendOnly"/> or <see cref="Direction.SendReceive"/>.</returns>
        public static bool HasSend(Direction dir)
        {
            return (dir == Direction.SendOnly) || (dir == Direction.SendReceive);
        }

        /// <summary>
        /// Check whether the given direction includes receiving.
        /// </summary>
        /// <param name="dir">The direction to check.</param>
        /// <returns><c>true</c> if direction is <see cref="Direction.ReceiveOnly"/> or <see cref="Direction.SendReceive"/>.</returns>
        public static bool HasRecv(Direction dir)
        {
            return (dir == Direction.ReceiveOnly) || (dir == Direction.SendReceive);
        }

        /// <summary>
        /// Check whether the given direction includes sending.
        /// </summary>
        /// <param name="dir">The direction to check.</param>
        /// <returns><c>true</c> if direction is <see cref="Direction.SendOnly"/> or <see cref="Direction.SendReceive"/>.</returns>
        public static bool HasSend(Direction? dir)
        {
            return dir.HasValue && ((dir == Direction.SendOnly) || (dir == Direction.SendReceive));
        }

        /// <summary>
        /// Check whether the given direction includes receiving.
        /// </summary>
        /// <param name="dir">The direction to check.</param>
        /// <returns><c>true</c> if direction is <see cref="Direction.ReceiveOnly"/> or <see cref="Direction.SendReceive"/>.</returns>
        public static bool HasRecv(Direction? dir)
        {
            return dir.HasValue && ((dir == Direction.ReceiveOnly) || (dir == Direction.SendReceive));
        }

        /// <summary>
        /// Compute a transceiver direction from some send/receive booleans.
        /// </summary>
        /// <param name="hasSend">Does the direction includes sending?</param>
        /// <param name="hasRecv">Does the direction includes receiving?</param>
        /// <returns>The computed transceiver direction.</returns>
        public static Direction DirectionFromSendRecv(bool hasSend, bool hasRecv)
        {
            return (hasSend ? (hasRecv ? Direction.SendReceive : Direction.SendOnly) : (hasRecv ? Direction.ReceiveOnly : Direction.Inactive));
        }
    }
}
